#include "ModelLoader.h"
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <algorithm>
#include <cstdio>
using namespace std;

ModelLoader * ModelLoader::instance = NULL;

ModelLoader * ModelLoader::get()
{
	if (!instance) 
		instance = new ModelLoader();
	return instance;
}

ModelLoader::ModelLoader(void)
{
}

ModelLoader::~ModelLoader(void)
{
}

Mesh * ModelLoader::loadModel(const char * fileName)
{
	string fileNameStr(fileName);
	string filePath;
	if (fileNameStr.find_last_of('\\') != std::string::npos)
	{
		filePath = fileNameStr.substr(0, fileNameStr.find_last_of('\\')+1);
	}
	
	ifstream f(fileName);
	if (!f.is_open())
	{
		cout << "File open failed : " << fileName << "\n";
		return NULL;
	}
	
	vector<string> lines;
	char buffer[256];
	while (!f.eof()) 
	{
		f.getline(buffer, 256);
		lines.push_back(string(buffer));
	}

	map<string, GLuint> materials;
	extractMaterials(lines, materials, filePath);

	Mesh * mesh = new Mesh();

	GLuint * currentMaterial = NULL;
	vector<string>::iterator it = lines.begin();

	for (; it != lines.end(); ++it)
	{
		string s = *it;
		
		if (s.empty()) continue;
		if (startsWith("# ", s) == true) continue;

		if (startsWith("v ", s) == true)
		{
			Vertex3D vertex;
			sscanf(s.c_str(), "v %f %f %f", &vertex.x, &vertex.y, &vertex.z);
			mesh->addVertex(vertex);
		}
		else if (startsWith("vt ", s) == true)
		{
			TexCoord2D texCoord;
			sscanf(s.c_str(), "vt %f %f", &texCoord.u, &texCoord.v);
			mesh->addTexCoord(texCoord);
		}
		else if (startsWith("vn ", s) == true)
		{
			// TBD normals
		}
		else if (startsWith("f ", s) == true)
		{
			FaceType mType = getMeshType(s);

			switch (mType)
			{
			case FACE_NOT_SUPPORTED : 
				{
					cout << "Unsupported mesh. Ignoring...\n";
					break;
				}
			case TRIANGLE_WITH_NORM_NO_UV:
				{
					Triangle face;
					if (currentMaterial != NULL)
					{
						face.textureId = new GLuint;
						*face.textureId = *currentMaterial;
					}
					face.vIndex = new int[3];
					face.nIndex = new int[3];
					sscanf(s.c_str(), "f %d//%d %d//%d %d//%d", 
						&face.vIndex[0], &face.nIndex[0], 
						&face.vIndex[1], &face.nIndex[1], 
						&face.vIndex[2], &face.nIndex[2]
					);
					face.vIndex[0]--; face.vIndex[1]--; face.vIndex[2]--;
					face.nIndex[0]--; face.nIndex[1]--; face.nIndex[2]--;
					mesh->addFace(face);
					break;
				}
			case TRIANGLE_WITH_NO_UV_NO_NORM:
				{
					Triangle face;
					if (currentMaterial != NULL)
					{
						face.textureId = new GLuint;
						*face.textureId = *currentMaterial;
					}
					face.vIndex = new int[3];
					sscanf(s.c_str(), "f %d %d %d", 
						&face.vIndex[0], &face.vIndex[1], &face.vIndex[2]
					);
					face.vIndex[0]--; face.vIndex[1]--; face.vIndex[2]--;
					mesh->addFace(face);
					break;
				}
			case TRIANGLE_WITH_UV_NORM:
				{
					Triangle face;
					if (currentMaterial != NULL)
					{
						face.textureId = new GLuint;
						*face.textureId = *currentMaterial;
					}
					face.vIndex = new int[3];
					face.tIndex = new int[3];
					face.nIndex = new int[3];
					sscanf(s.c_str(), "f %d/%d/%d %d/%d/%d %d/%d/%d", 
						&face.vIndex[0], &face.tIndex[0], &face.nIndex[0],
						&face.vIndex[1], &face.tIndex[1], &face.nIndex[1],
						&face.vIndex[2], &face.tIndex[2], &face.nIndex[2]
					);
					face.vIndex[0]--; face.vIndex[1]--; face.vIndex[2]--;
					face.tIndex[0]--; face.tIndex[1]--; face.tIndex[2]--;
					face.nIndex[0]--; face.nIndex[1]--; face.nIndex[2]--;
					mesh->addFace(face);
					break;
				}
			case TRIANGLE_WITH_UV_NO_NORM:
				{
					Triangle face;
					if (currentMaterial != NULL)
					{
						face.textureId = new GLuint;
						*face.textureId = *currentMaterial;
					}
					face.vIndex = new int[3];
					face.tIndex = new int[3];
					sscanf(s.c_str(), "f %d/%d %d/%d %d/%d", 
						&face.vIndex[0], &face.tIndex[0], 
						&face.vIndex[1], &face.tIndex[1], 
						&face.vIndex[2], &face.tIndex[2]
					);
					face.vIndex[0]--; face.vIndex[1]--; face.vIndex[2]--;
					face.tIndex[0]--; face.tIndex[1]--; face.tIndex[2]--;
					mesh->addFace(face);
					break;
				}
			case QUAD_WITH_NORM_NO_UV:
			case QUAD_WITH_NO_UV_NO_NORM:
			case QUAD_WITH_UV_NORM:
			case QUAD_WITH_UV_NO_NORM:
				{
					cout << "QUAD faces not implemented yet\n";
					break;
				}
			default:
				break;
			};
		}
		else if (startsWith("usemtl ", s) == true)
		{
			char mtlName[256];
			sscanf(s.c_str(), "usemtl %s", mtlName);
			
			map<string, GLuint>::iterator it = materials.find(mtlName);
			if (it != materials.end())
			{
				currentMaterial = new GLuint;
				*currentMaterial = materials[mtlName];
				cout << "Current material : " << mtlName << "\n";
			}
		}
	}

	return mesh;
}

void ModelLoader::extractMaterials(vector<string>& lines, map<string, GLuint>& materials, string filePath)
{
	vector<string>::iterator it = lines.begin();
	for (; it != lines.end(); ++it)
	{
		if ((*it).empty()) 
			continue;
		if (startsWith("mtllib", *it) == true)
		{
			char mtlFileName[256];
			sscanf((*it).c_str(), "mtllib %s", mtlFileName);
			processMaterials(materials, mtlFileName, filePath);
		}
	}
}

void ModelLoader::processMaterials(map<string, GLuint>& materials, string fileName, string filePath)
{
	string fullPath = filePath + fileName;
	ifstream f(fullPath);
	if (!f.is_open())
	{
		cout << "File open failed : " << fullPath << "\n";
		return;
	}
	
	vector<string> lines;
	char buffer[256];
	while (!f.eof()) 
	{
		f.getline(buffer, 256);
		lines.push_back(string(buffer));
	}

	int found = 0;
	string matName;
	string texName;
	for (unsigned int i=0; i<lines.size(); ++i)
	{
		string line = lines.at(i);
		if (startsWith("newmtl", line) == true)
		{
			sscanf(line.c_str(), "newmtl %s", buffer);
			matName = buffer;
			found++;
		}
		else if (startsWith("map_Kd", line) == true)
		{
			sscanf(line.c_str(), "map_Kd %s", buffer);
			texName = buffer;
			found++;
		}

		if (found == 2)
		{
			found = 0;
			string textureFileName = filePath + texName;
			cout << matName << " => " << textureFileName << "\n";
			materials[matName] = loadTexture(textureFileName.c_str());
		}
	}
}

GLuint ModelLoader::loadTexture(const char * fileName)
{
	GLuint glId;
	glGenTextures(1, &glId);
	SDL_Surface * image = IMG_Load(fileName);

	if (image == NULL) {
		cout << "Cannot load file : " << fileName << "\n";
		return -1;
	}

	SDL_PixelFormat format = {
		NULL, 
		32, 
		4, 
		0, 0, 0, 0, 0, 0, 0, 0, 
		0xff000000,
		0x00ff0000,
		0x0000ff00,
		0x000000ff,
		0,
		255
	};
	
	SDL_Surface * img = SDL_ConvertSurface(image, &format, SDL_SWSURFACE);

	if (img == NULL) {
		cout << "Conversion failed : " << fileName << "\n";
		return -1;
	}

	glBindTexture(GL_TEXTURE_2D, glId);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
	glTexImage2D(
		GL_TEXTURE_2D, 
		0, 
		GL_RGBA, 
		img->w, 
		img->h,
		0, 
		GL_RGBA, 
		GL_UNSIGNED_INT_8_8_8_8, 
		img->pixels
	);
	SDL_FreeSurface(img);
	SDL_FreeSurface(image);

	return glId;
}

bool ModelLoader::startsWith(string starter, string text)
{
	if (starter.length() > text.length())
		return false;

	string::iterator starterIt = starter.begin();
	string::iterator textIt = text.begin();

	for (; starterIt != starter.end(); ++starterIt, ++textIt)
	{
		if (*starterIt != *textIt)
			return false;
	}
	return true;
}

ModelLoader::FaceType ModelLoader::getMeshType(string str)
{
	FaceType mType = FACE_NOT_SUPPORTED;

	if (count(str.begin(), str.end(), '/') == 0)
	{
		if (count(str.begin(), str.end(), ' ') == 3)
		{
			mType = TRIANGLE_WITH_NO_UV_NO_NORM;
		}
		else if (count(str.begin(), str.end(), ' ') == 4)
		{
			mType = QUAD_WITH_NO_UV_NO_NORM;
		}
	}
	else
	{
		if (count(str.begin(), str.end(), '/') == 3)
		{
			mType = TRIANGLE_WITH_UV_NO_NORM;
		}
		else if (count(str.begin(), str.end(), '/') == 6)
		{
			mType = (str.find("//") == string::npos) ? 
				TRIANGLE_WITH_UV_NORM : TRIANGLE_WITH_NORM_NO_UV;
		}
		else if (count(str.begin(), str.end(), '/') == 4)
		{
			mType = QUAD_WITH_UV_NO_NORM;
		}
		else if (count(str.begin(), str.end(), '/') == 8)
		{
			mType = (str.find("//") != string::npos) ? 
				QUAD_WITH_UV_NORM : QUAD_WITH_NORM_NO_UV;
		}
	}

	return mType;
}
